/*
 * Copyright 2017 Google Inc. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.blockly.ohos;

import com.google.blockly.ohos.codegen.CodeGenerationRequest;
import com.google.blockly.ohos.codegen.LanguageDefinition;
import com.google.blockly.ohos.control.BlocklyController;
import com.google.blockly.ohos.ui.BlockViewFactory;
import com.google.blockly.ohos.ui.MutatorFraction;
import com.google.blockly.model.BlockExtension;
import com.google.blockly.model.CustomCategory;
import com.google.blockly.model.DefaultBlocks;
import com.google.blockly.model.Mutator;
import com.google.blockly.utils.LangUtils;
import com.google.blockly.utils.LogUtils;
import ohos.aafwk.ability.fraction.Fraction;
import ohos.aafwk.content.Intent;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.agp.components.LayoutScatter;
import ohos.agp.utils.LayoutAlignment;
import ohos.agp.window.dialog.PopupDialog;

import java.util.List;


/**
 * Base class for a Blockly fraction containing the workspace, toolbar, and trash flyout.
 * returns true, the ability will attempt to load a prior workspace from the instance state
 * bundle.  If no workspace is loaded, it defers to {@link #onLoadInitialWorkspace}.
 * <p/>
 * Configure the workspace by providing definitions for {@link #getBlockDefinitionsJsonPaths()} and
 * {@link #getToolboxContentsXmlPath()}. Alternate {@link BlockViewFactory}s are supported via
 * {@link #onLoadInitialWorkspace()}.
 * <p/>
 * The block definitions can be updated at any time by calling {@link #resetBlockFactory()},
 * which triggers another call to {@link #getBlockDefinitionsJsonPaths()}.  Similarly, The toolbox
 * can be reloaded by calling  {@link #reloadToolbox()}, which triggers another call to
 * {@link #getToolboxContentsXmlPath()}.
 * <p/>
 * By default, this fraction generates its own menu options. This can be disabled by calling
 * {@code setHasOptionsMenu(false)} or customized by overriding.
 */
public abstract class AbstractBlocklyFraction extends Fraction implements Component.ClickedListener {
    private static final String TAG = "AbstractBlocklyFraction";

    protected Component mRootView = null;
    protected BlocklyAbilityHelper mBlocklyAbilityHelper = null;
    protected PopupDialog mMenuDialog = null;

    @Override
    public void onClick(Component component) {
        int id = component.getId();
        if (id == ResourceTable.Id_action_save) {
            try {
                onSaveWorkspace();
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else if (id == ResourceTable.Id_action_load) {
            onLoadWorkspace();
        } else if (id == ResourceTable.Id_action_more) {
            showMenuComponent();
        } else if (id == ResourceTable.Id_action_clear) {
            mMenuDialog.hide();
            onClearWorkspace();
        } else if (id == ResourceTable.Id_action_run) {
            mMenuDialog.hide();
            if (getController().getWorkspace().hasBlocks()) {
                onRunCode();
            } else {
                LogUtils.i(TAG, "No blocks in workspace. Skipping run request.");
            }
        }
    }

    private void showMenuComponent() {
        if (mMenuDialog == null) {
            Component menu = LayoutScatter.getInstance(getContext()).parse(getDefaultMenuLayoutRes(), null, false);
            menu.findComponentById(ResourceTable.Id_action_clear).setClickedListener(this);
            menu.findComponentById(ResourceTable.Id_action_run).setClickedListener(this);
            mMenuDialog = new PopupDialog(getContext(), null);
            mMenuDialog.setCustomComponent(menu);
            mMenuDialog.setAlignment(LayoutAlignment.RIGHT | LayoutAlignment.TOP);
            mMenuDialog.setDialogListener(() -> {
                mMenuDialog.hide();
                return false;
            });
        }
        if (!mMenuDialog.isShowing()) {
            mMenuDialog.show();
        }
    }

    public int getDefaultMenuLayoutRes() {
        return ResourceTable.Layout_blockly_default_menu;
    }


    /**
     * Override to use a different language File in ms/js (Defaults to "en")
     * <p>
     * Blockly two letter language codes differ slightly than the Ohos recognized set.
     * While the vast majority of users will not have an issue, some edge dialects may differ.
     * See files (and filenames) in https://github.com/google/blockly/tree/master/msg/js for the
     * latest available set.
     *
     * @return Selected Language
     */
    public String getLang() {
        return "en";
    }

    protected Component onComponentAttached(LayoutScatter scatter, ComponentContainer container, Intent intent) {
        LangUtils.setLang(getLang());
        mRootView = scatter.parse(ResourceTable.Layout_drawers_and_action_bar, container, false);
        mRootView.findComponentById(ResourceTable.Id_action_save).setClickedListener(this);
        mRootView.findComponentById(ResourceTable.Id_action_load).setClickedListener(this);
        mRootView.findComponentById(ResourceTable.Id_action_more).setClickedListener(this);
        return mRootView;
    }

    /**
     * Called when the user clicks the save action.  Default implementation delegates handling to
     * {@link #getWorkspaceSavePath()}.
     */
    public void onSaveWorkspace() {
        mBlocklyAbilityHelper.saveWorkspaceToAppDirSafely(getWorkspaceSavePath());
    }

    /**
     * Called when the user clicks the load action.  Default implementation delegates handling to
     * {@link #getWorkspaceSavePath()}..
     */
    public void onLoadWorkspace() {
        mBlocklyAbilityHelper.loadWorkspaceFromAppDirSafely(getWorkspaceSavePath());
    }

    /**
     * Called when the user clicks the clear action.  Default implementation resets the
     * workspace, removing all blocks from the workspace, and then calls
     * {@link #onInitBlankWorkspace()}.
     */
    public void onClearWorkspace() {
        getController().resetWorkspace();
        onInitBlankWorkspace();
    }

    /**
     * @return The {@link BlocklyController} controlling the workspace in this ability.
     */
    public final BlocklyController getController() {
        return mBlocklyAbilityHelper.getController();
    }

    /**
     * Propagate back event to BlocklyAbilityHelper to close Blockly trash or toolbox flyouts.
     *
     * @return Whether the back event triggered closing a Blockly flyout.
     */
    public boolean onBackPressed() {
        // Try to close any open drawer / toolbox before backing out of the ability.
        return mBlocklyAbilityHelper.onBackToCloseFlyouts();
    }

    /**
     * Create a {@link BlocklyAbilityHelper} to use for this ability.
     */
    protected BlocklyAbilityHelper onCreateAbilityHelper() {
        return new BlocklyAbilityHelper(getFractionAbility(), getFractionAbility().getFractionManager(), getScrollOrientation(), getCloseableStatus());
    }

    /**
     * Decide the scroll orientation
     */
    protected int getScrollOrientation() {
        return Component.VERTICAL;
    }

    /**
     * Decide if it can be closed
     */
    protected boolean getCloseableStatus() {
        return true;
    }

    /**
     * Propagate lifecycle event to BlocklyAbilityHelper.
     */
    @Override
    protected void onStart(Intent intent) {
        super.onStart(intent);
    }

    /**
     * Propagate lifecycle event to BlocklyAbilityHelper.
     */
    @Override
    protected void onInactive() {
        super.onInactive();
        mBlocklyAbilityHelper.onPause();
        onAutosave();
    }

    /**
     * Propagate lifecycle event to BlocklyAbilityHelper.
     */
    @Override
    protected void onActive() {
        super.onActive();
        if (mBlocklyAbilityHelper == null) {
            mBlocklyAbilityHelper = onCreateAbilityHelper();

            if (mBlocklyAbilityHelper == null) {
                throw new IllegalStateException("BlocklyAbilityHelper is null. "
                        + "onCreateAbilityHelper must return a instance.");
            }
            resetBlockFactory();  // Initial load of block definitions, extensions, and mutators.
            configureCategoryFactories();  // After BlockFactory; before Toolbox
            reloadToolbox();
            onLoadInitialWorkspace();
        }
        mBlocklyAbilityHelper.onStart();
        mBlocklyAbilityHelper.onResume();
    }

    /**
     * Propagate lifecycle event to BlocklyAbilityHelper.
     */
    @Override
    public void onStop() {
        super.onStop();
        mBlocklyAbilityHelper.onStop();
    }

    /**
     * Hook for subclasses to load an initial workspace. Default implementation just calls
     * {@link #onInitBlankWorkspace()}.
     */
    protected void onLoadInitialWorkspace() {
        onInitBlankWorkspace();
        getController().closeFlyouts();
    }

    /**
     * Called when an autosave of the workspace is triggered, typically.
     * By default this saves the workspace to a file in the app's directory.
     */
    protected void onAutosave() {
        try {
            mBlocklyAbilityHelper.saveWorkspaceToAppDir(getWorkspaceAutosavePath());
        } catch (Exception e) {
            LogUtils.e(TAG, "Failed to autosaving workspace.");
        }
    }

    /**
     * Hook for subclasses to initialize a new blank workspace. Initialization may include
     * configuring default variables or other setup.
     */
    protected void onInitBlankWorkspace() {
    }

    /**
     * @return The asset path for the xml toolbox config.
     */

    abstract protected String getToolboxContentsXmlPath();

    /**
     * @return The asset path for the json block definitions.
     */

    abstract protected List<String> getBlockDefinitionsJsonPaths();

    /**
     * @return The asset path for the core language file used to generate code.
     */

    protected LanguageDefinition getBlockGeneratorLanguage() {
        return DefaultBlocks.LANGUAGE_DEFINITION;
    }

    /**
     * This method provides a hook to register {@link BlockExtension}s that support the block
     * definitions in this ability. By default, it adds all extensions in
     * {@link DefaultBlocks#getExtensions() DefaultBlocks} to the block factory, via the
     * <p/>
     * Extensions with the same key will replace existing extensions, so it is safe
     * to call super and then update specific extensions.
     * <p/>
     * Called from {@link #resetBlockFactory()}.
     */
    protected void configureBlockExtensions() {
        mBlocklyAbilityHelper.configureExtensions();
    }

    /**
     * This method provides a hook to register {@link Mutator.Factory}s and
     * {@link MutatorFraction.Factory}s that support the block definitions in this ability. By
     * default, it adds the mutators in {@link DefaultBlocks#getMutators() DefaultBlocks} to the
     * <p/>
     * Mutators with the same key will replace existing mutators, so it is safe
     * to call super and then update specific mutators.
     * <p/>
     * Called from {@link #resetBlockFactory()}.
     */
    protected void configureMutators() {
        mBlocklyAbilityHelper.configureMutators();
    }

    /**
     * This method provides a hook to register custom {@link CustomCategory}s that support
     * the toolboxes in this ability. By default, it registers the categories in
     * <p/>
     * Category factories with the same {@code custom} key will replace existing
     * {@link CustomCategory}s, so it is safe to call super and then update specific categories.
     * <p/>
     * Called once at ability creation.
     */
    protected void configureCategoryFactories() {
        mBlocklyAbilityHelper.configureCategoryFactories();
    }

    /**
     * Returns the asset file paths to the generators (JS files) to use for the most
     * recently requested "Run" action. Called from {@link #onRunCode()}. This is expected to be a
     * list of JavaScript files that contain the block generators.
     *
     * @return The list of file paths to the block generators.
     */

    abstract protected List<String> getGeneratorsJsPaths();

    /**
     * Returns a generation callback to use for the most recently requested "Run" action.
     * Called from {@link #onRunCode()}.
     *
     * @return The generation callback.
     */

    abstract protected CodeGenerationRequest.CodeGeneratorCallback getCodeGenerationCallback();

    /**
     * @return The path to the saved workspace file on the local device. By default,
     * "workspace.xml".
     */

    protected String getWorkspaceSavePath() {
        return "workspace.xml";
    }

    /**
     * @return The path to the automatically saved workspace file on the local device. By default,
     * "autosave_workspace.xml".
     */

    protected String getWorkspaceAutosavePath() {
        return "autosave_workspace.xml";
    }

    /**
     * Runs the code generator. Called when user selects "Run" action.
     * <p/>
     * Gets the latest block definitions and generator code by calling
     * {@link #getBlockDefinitionsJsonPaths()} and {@link #getGeneratorsJsPaths()} just before
     * invoking generation.
     *
     * @see #getCodeGenerationCallback()
     */
    protected void onRunCode() {
        mBlocklyAbilityHelper.requestCodeGeneration(
                getBlockGeneratorLanguage(),
                getBlockDefinitionsJsonPaths(),
                getGeneratorsJsPaths(),
                getCodeGenerationCallback());
    }

    /**
     * Reloads the toolbox contents using the path provided by {@link #getToolboxContentsXmlPath()}.
     */
    protected void reloadToolbox() {
        mBlocklyAbilityHelper.reloadToolbox(getToolboxContentsXmlPath());
    }

    /**
     * Reloads the block definitions, including extensions and mutators. Calls
     */
    protected void resetBlockFactory() {
        mBlocklyAbilityHelper.resetBlockFactory(getBlockDefinitionsJsonPaths());
        configureBlockExtensions();
        configureMutators();
        configureCategoryFactories();
        // Reload the toolbox?
    }
}
